<?php
namespace TYPO3\CMS\Fluid\Service;

/*                                                                        *
 * This script is backported from the TYPO3 Flow package "TYPO3.Fluid".   *
 *                                                                        *
 * It is free software; you can redistribute it and/or modify it under    *
 * the terms of the GNU Lesser General Public License, either version 3   *
 *  of the License, or (at your option) any later version.                *
 *                                                                        *
 * The TYPO3 project - inspiring people to share!                         *
 *                                                                        */

use TYPO3\CMS\Fluid\Core\ViewHelper\ViewHelperInterface;

/**
 * XML Schema (XSD) Generator. Will generate an XML schema which can be used for autocompletion
 * in schema-aware editors like Eclipse XML editor.
 */
class DocbookGenerator extends \TYPO3\CMS\Fluid\Service\AbstractGenerator
{
    /**
     * Generate the XML Schema definition for a given namespace.
     *
     * @param string $namespace Namespace identifier to generate the XSD for, without leading Backslash.
     * @return string XML Schema definition
     */
    public function generateDocbook($namespace)
    {
        if (substr($namespace, -1) !== \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR) {
            $namespace .= \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR;
        }
        $classNames = $this->getClassNamesInNamespace($namespace);
        $xmlRootNode = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
<section version="5.0" xmlns="http://docbook.org/ns/docbook"
         xml:id="fluid.usermanual.standardviewhelpers"
         xmlns:xl="http://www.w3.org/1999/xlink"
         xmlns:xi="http://www.w3.org/2001/XInclude"
         xmlns:xhtml="http://www.w3.org/1999/xhtml"
         xmlns:svg="http://www.w3.org/2000/svg"
         xmlns:ns="http://docbook.org/ns/docbook"
         xmlns:mathml="http://www.w3.org/1998/Math/MathML">
    <title>Standard View Helper Library</title>

    <para>Should be autogenerated from the tags.</para>
</section>');
        foreach ($classNames as $className) {
            $this->generateXmlForClassName($className, $namespace, $xmlRootNode);
        }
        return $xmlRootNode->asXML();
    }

    /**
     * Generate the XML Schema for a given class name.
     *
     * @param string $className Class name to generate the schema for.
     * @param string $namespace Namespace prefix. Used to split off the first parts of the class name.
     * @param \SimpleXMLElement $xmlRootNode XML root node where the xsd:element is appended.
     * @return void
     */
    protected function generateXmlForClassName($className, $namespace, \SimpleXMLElement $xmlRootNode)
    {
        $reflectionClass = new \TYPO3\CMS\Extbase\Reflection\ClassReflection($className);
        if (!$reflectionClass->isSubclassOf($this->abstractViewHelperReflectionClass)) {
            return;
        }
        $tagName = $this->getTagNameForClass($className, $namespace);
        $docbookSection = $xmlRootNode->addChild('section');
        $docbookSection->addChild('title', $tagName);
        $this->docCommentParser->parseDocComment($reflectionClass->getDocComment());
        $this->addDocumentation($this->docCommentParser->getDescription(), $docbookSection);
        $argumentsSection = $docbookSection->addChild('section');
        $argumentsSection->addChild('title', 'Arguments');
        $this->addArguments($className, $argumentsSection);
        return $docbookSection;
    }

    /**
     * Add attribute descriptions to a given tag.
     * Initializes the view helper and its arguments, and then reads out the list of arguments.
     *
     * @param string $className Class name where to add the attribute descriptions
     * @param \SimpleXMLElement $docbookSection DocBook section to add the attributes to.
     * @return void
     */
    protected function addArguments($className, \SimpleXMLElement $docbookSection)
    {
        $viewHelper = $this->instanciateViewHelper($className);
        $argumentDefinitions = $viewHelper->prepareArguments();
        if (count($argumentDefinitions) === 0) {
            $docbookSection->addChild('para', 'No arguments defined.');
            return;
        }
        $argumentsTable = $docbookSection->addChild('table');
        $argumentsTable->addChild('title', 'Arguments');
        $tgroup = $argumentsTable->addChild('tgroup');
        $tgroup['cols'] = 4;
        $this->addArgumentTableRow($tgroup->addChild('thead'), 'Name', 'Type', 'Required', 'Description', 'Default');
        $tbody = $tgroup->addChild('tbody');
        foreach ($argumentDefinitions as $argumentDefinition) {
            $this->addArgumentTableRow($tbody, $argumentDefinition->getName(), $argumentDefinition->getType(), $argumentDefinition->isRequired() ? 'yes' : 'no', $argumentDefinition->getDescription(), $argumentDefinition->getDefaultValue());
        }
    }

    /**
     * Instantiate a view helper.
     *
     * @param string $className
     * @return ViewHelperInterface
     */
    protected function instanciateViewHelper($className)
    {
        return $this->objectManager->get($className);
    }

    /**
     * @param \SimpleXMLElement $parent
     * @param string $name
     * @param string $type
     * @param bool $required
     * @param string $description
     * @param string $default
     * @return void
     */
    private function addArgumentTableRow(\SimpleXMLElement $parent, $name, $type, $required, $description, $default)
    {
        $row = $parent->addChild('row');
        $row->addChild('entry', $name);
        $row->addChild('entry', $type);
        $row->addChild('entry', $required);
        $row->addChild('entry', $description);
        $row->addChild('entry', (string)$default);
    }

    /**
     * Add documentation XSD to a given XML node
     *
     * As Eclipse renders newlines only on new <xsd:documentation> tags, we wrap every line in a new
     * <xsd:documentation> tag.
     * Furthermore, eclipse strips out tags - the only way to prevent this is to have every line wrapped in a
     * CDATA block AND to replace the < and > with their XML entities. (This is IMHO not XML conformant).
     *
     * @param string $documentation Documentation string to add.
     * @param \SimpleXMLElement $docbookSection Node to add the documentation to
     * @return void
     */
    protected function addDocumentation($documentation, \SimpleXMLElement $docbookSection)
    {
        $splitRegex = '/^\\s*(=[^=]+=)$/m';
        $regex = '/^\\s*(=([^=]+)=)$/m';
        $matches = preg_split($splitRegex, $documentation, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        $currentSection = $docbookSection;
        foreach ($matches as $singleMatch) {
            if (preg_match($regex, $singleMatch, $tmp)) {
                $currentSection = $docbookSection->addChild('section');
                $currentSection->addChild('title', trim($tmp[2]));
            } else {
                $this->addText(trim($singleMatch), $currentSection);
            }
        }
    }

    /**
     * @param string $text
     * @param \SimpleXMLElement $parentElement
     */
    protected function addText($text, \SimpleXMLElement $parentElement)
    {
        $splitRegex = '/
		(<code(?:.*?)>
			(?:.*?)
		<\\/code>)/xs';
        $regex = '/
		<code(.*?)>
			(.*?)
		<\\/code>/xs';
        $matches = preg_split($splitRegex, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        foreach ($matches as $singleMatch) {
            if (preg_match($regex, $singleMatch, $tmp)) {
                preg_match('/title="([^"]+)"/', $tmp[1], $titleMatch);
                $example = $parentElement->addChild('example');
                if (count($titleMatch)) {
                    $example->addChild('title', trim($titleMatch[1]));
                } else {
                    $example->addChild('title', 'Example');
                }
                $this->addChildWithCData($example, 'programlisting', trim($tmp[2]));
            } else {
                $textParts = explode("\n", $singleMatch);
                foreach ($textParts as $text) {
                    if (trim($text) === '') {
                        continue;
                    }
                    $this->addChildWithCData($parentElement, 'para', trim($text));
                }
            }
        }
    }
}
